/** \file gu_material.cpp */

#include "gu_material.h"
#include "gu_shader.h"
#include "gr_algorithms.h"

#include "gr_math.h"

#include <functional>
#include <vector>
#include <tuple>
#include <memory>
#include <exception>
#include <cassert>

namespace gu{


class VarScalar : public MaterialVariable
{
public:
    float data;

    VarScalar(const Variable& signature, ShaderLoc location): 
        MaterialVariable(signature, location), data(1.0f){type = SCALAR;}
    float* scalar(){return &data;}
    void set(float s){data = s;}
    void apply(UniformMap* umap){
        umap->mapTo(location_, data);
    }
};

class VarVec3 : public MaterialVariable
{
public:
    gr::vec3 data;
    VarVec3(const Variable& signature, ShaderLoc location): 
        MaterialVariable(signature, location), data(1.0f, 0.0f, 0.0f){type = VEC3;}
    gr::vec3* vec3(){return &data;}
    void set(gr::vec3& v){data = v;}
    void apply(UniformMap* umap){
        umap->mapTo(location_, data);
    }
};

class VarVec4 : public MaterialVariable
{
public:
    gr::vec4 data;
    VarVec4(const Variable& signature, ShaderLoc location): 
        MaterialVariable(signature, location), data(1.0f, 0.0f, 0.0f, 0.0f){type = VEC4;}
    gr::vec4* vec4(){return &data;}
    void set(gr::vec4& v){data = v;}
    void apply(UniformMap* umap){
        umap->mapTo(location_, data);
    }
};

class VarMat4 : public MaterialVariable
{
public:
    gr::mat4 data;
    VarMat4(const Variable& signature, ShaderLoc location):
        MaterialVariable(signature, location)
    {
        data = gr::identity<gr::mat4>();
        type = MAT4;
    }
    gr::mat4* mat4(){return &data;}
    void set(gr::mat4& v){data = v;}
    void apply(UniformMap* umap){
        umap->mapTo(location_, data);
    }
};

MaterialVariable::shared_ptr materialVariable(const Variable& signature, ShaderLoc location)
{
    MaterialVariable::shared_ptr ptr;

    switch(signature.type)
    {
        case SCALAR:ptr.reset(new VarScalar(signature, location)); break;
        case VEC3:ptr.reset(new VarVec3(signature, location)); break;
        case VEC4:ptr.reset(new VarVec4(signature, location)); break;
        case MAT4:ptr.reset(new VarMat4(signature, location)); break;
    }
    return ptr;
}

StreamSlot::shared_ptr streamSlot(const Variable& signature, ShaderLoc location)
{
    StreamSlot::shared_ptr slot(new StreamSlot(signature, location));
    return slot;
}

class MaterialImp
{
public:


    Shader::shared_ptr shader_;

    // variables, locations, names
    MaterialVarMap vars_;
    StreamSlotMap streams_;
    LocMap locs_;
    LocMap streamLocs_;

    MaterialImp(Shader::shared_ptr shader):shader_(shader)
    {}

    void init()
    {
        auto variables = shader_->variableMap();
        
        auto initVars= [&](Variable& var)
        {
            if(var.mapping == UNIFORM)
            {
                auto location = shader_->getUniformLocation(var.name.c_str());
                locs_[var.name] = location;
                vars_[var.name] = materialVariable(var, location);
            }
            else if(var.mapping == IN)
            {
                auto location = shader_->getAttribLocation(var.name.c_str());
                streamLocs_[var.name] = location;
                streams_[var.name] = streamSlot(var, location);
            }
        };
        gr::forEachValue(variables, initVars);
    }

    void mapValues(UniformMap* umap)
    {
        auto apply = [&](MaterialVariable::shared_ptr var)
        {
            var->apply(umap);
        };

        gr::forEachValue(vars_, apply);
    }

};

Material::Material():impl_(nullptr)
{
}
Material::~Material()
{
    if(impl_) delete impl_;
}

Shader* Material::shader()
{
    return impl_->shader_.get();
}

/** Transform uniform data through uniform map. */
void Material::use(UniformMap* umap)
{
    impl_->mapValues(umap);
}

MaterialVarMap& Material::uniforms()
{
    return impl_->vars_;
}

StreamSlotMap&  Material::inputStreams()
{
    return impl_->streams_;
}

LocMap& Material::uniformLocMap()
{
    return impl_->locs_;
}

LocMap& Material::inputStreamLocMap()
{
    return impl_->streamLocs_;
}


// TODO: What is the correct place to define our exceptions
class ShaderException : public std::runtime_error {
public:
  ShaderException() : std::runtime_error("Shader compilation failed") { }
};
 
Material* newGlMaterial(const char* vertexShader, const char* fragmentShader)
{
    Material* material = nullptr;

    Shader::shared_ptr shader(gu::newShader(gu::GLSL_330));

    shader->setSource(gu::FRAGMENT, fragmentShader);
    shader->setSource(gu::VERTEX, vertexShader);

    if(! shader->compile())
    {
        throw ShaderException();
    }

    material = new Material();
    auto impl = new MaterialImp(shader);

    material->impl_ = impl;
    material->impl_->init(); // init uniform data

    return material;
}

}